前言

  • 系统一大,就会拆分成多个独立的进程,比如使用微服务架构,也就成了分布式系统。
  • 传统的日志系统比如log4j已经无法满足,我们需要将这些日志合并展示到一个统一的地方。
  • 如此一来,分布式日志收集系统就登场了。
  • 现在用的较多的技术组合为 ElasticSearch+ logstash(基于java)+kibana(基于JRuby, logstash已自带),也就是大家常说的ELK
  • 但是此系统较为重量级并不是很适合轻量级微服务架构,SpringBlade封装了一个相对好拓展的日志系统,下面我们来具体看一下把!~

实现思路

全局统一异常

  • 使用@RestControllerAdvice,捕获异常后统一返回封装好的格式。
  • 异常分成两种:已知异常与未知异常,未知异常是着重需要关注的,所以会将未知异常入库,方便排查
  • 以下为全局异常核心代码
@Slf4j
@Order
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@RestControllerAdvice
public class BladeRestExceptionTranslator {

   @ExceptionHandler(ServiceException.class)
   @ResponseStatus(HttpStatus.BAD_REQUEST)
   public R handleError(ServiceException e) {
      log.error("业务异常", e);
      return R.fail(e.getResultCode(), e.getMessage());
   }

   @ExceptionHandler(SecureException.class)
   @ResponseStatus(HttpStatus.UNAUTHORIZED)
   public R handleError(SecureException e) {
      log.error("认证异常", e);
      return R.fail(e.getResultCode(), e.getMessage());
   }

   @ExceptionHandler(Throwable.class)
   @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
   public R handleError(Throwable e) {
      log.error("服务器异常", e);
      //发送服务异常事件
      ErrorLogPublisher.publishEvent(e, UrlUtil.getPath(WebUtil.getRequest().getRequestURI()));
      return R.fail(ResultCode.INTERNAL_SERVER_ERROR, (Func.isEmpty(e.getMessage()) ? ResultCode.INTERNAL_SERVER_ERROR.getMessage() : e.getMessage()));
   }

}

  • 大家可以看下最后几行的 ErrorLogPublisher.publishEvent方法,可以看出,该方法使用了Spring的Event事件驱动,进行解耦。异步操作日志的收集、入库等。代码如下
/**
 * 异常信息事件发送
 *
 * @author Chill
 */
public class ErrorLogPublisher {

   public static void publishEvent(Throwable error, String requestUri) {
      HttpServletRequest request = WebUtil.getRequest();
      LogError logError = new LogError();
      logError.setRequestUri(requestUri);
      if (Func.isNotEmpty(error)) {
         logError.setStackTrace(Exceptions.getStackTraceAsString(error));
         logError.setExceptionName(error.getClass().getName());
         logError.setMessage(error.getMessage());
         StackTraceElement[] elements = error.getStackTrace();
         if (Func.isNotEmpty(elements)) {
            StackTraceElement element = elements[0];
            logError.setMethodName(element.getMethodName());
            logError.setMethodClass(element.getClassName());
            logError.setFileName(element.getFileName());
            logError.setLineNumber(element.getLineNumber());
         }
      }
      LogAbstractUtil.addRequestInfoToLog(request, logError);
      Map<String, Object> event = new HashMap<>(16);
      event.put(EventConstant.EVENT_LOG, logError);
      SpringUtil.publishEvent(new ErrorLogEvent(event));
   }

}
  • 跟踪到 SpringUtil.publishEvent(new ErrorLogEvent(event));
  • 查看 ErrorLogEvent代码
/**
 * 错误日志事件
 *
 * @author Chill
 */
public class ErrorLogEvent extends ApplicationEvent {

   public ErrorLogEvent(Map<String, Object> source) {
      super(source);
   }

}
  • 对应错误日志处理ErrorLogListener代码
/**
 * 异步监听错误日志事件
 *
 * @author Chill
 */
@Slf4j
@AllArgsConstructor
public class ErrorLogListener {

   private final ILogClient logService;
   private final ServerInfo serverInfo;
   private final BladeProperties bladeProperties;

   @Async
   @Order
   @EventListener(ErrorLogEvent.class)
   public void saveErrorLog(ErrorLogEvent event) {
      Map<String, Object> source = (Map<String, Object>) event.getSource();
      LogError logError = (LogError) source.get(EventConstant.EVENT_LOG);
      LogAbstractUtil.addOtherInfoToLog(logError, bladeProperties, serverInfo);
      logService.saveErrorLog(logError);
   }

}
  • 到了ErrorLogListener其实就可以进行拓展了,可以像现在这样,直接调用日志接口入库,也可以集成消息队列,提高性能。最终,我们可以到数据库中看到如下数据

  • 错误产生在哪个服务名,服务的环境,服务的ip,服务的host,异常堆栈详情,异常类,异常所在方法,所在行数,传的参数,操作人,操作时间等等信息一目了然。
  • 简单的几处代码,如若集成消息队列或redis,我相信足以满足轻量级微服务架构的需求。

API日志

  • 自动生成错误日志的讲完了,我们需要根据注解主动生成日志。
  • 对于这一类我们一般使用AOP来实现功能,切到对应注解后,获取当前请求的信息,然后再通过Event异步入库
  • AOP代码如下
/**
 * 操作日志使用spring event异步入库
 *
 * @author Chill
 */
@Slf4j
@Aspect
public class ApiLogAspect {

   @Around("@annotation(apiLog)")
   public Object around(ProceedingJoinPoint point, ApiLog apiLog) throws Throwable {
      //获取类名
      String className = point.getTarget().getClass().getName();
      //获取方法
      String methodName = point.getSignature().getName();
      // 发送异步日志事件
      long beginTime = System.currentTimeMillis();
      //执行方法
      Object result = point.proceed();
      //执行时长(毫秒)
      long time = System.currentTimeMillis() - beginTime;
      //记录日志
      ApiLogPublisher.publishEvent(methodName, className, apiLog, time);
      return result;
   }

}
  • 注解代码如下
/**
 * 操作日志注解
 *
 * @author Chill
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLog {

   /**
    * 日志描述
    *
    * @return {String}
    */
   String value() default "日志记录";
}

  • 日志发送器代码如下
/**
 * API日志信息事件发送
 *
 * @author Chill
 */
public class ApiLogPublisher {

   public static void publishEvent(String methodName, String methodClass, ApiLog apiLog, long time) {
      HttpServletRequest request = WebUtil.getRequest();
      LogApi logApi = new LogApi();
      logApi.setType(BladeConstant.LOG_NORMAL_TYPE);
      logApi.setTitle(apiLog.value());
      logApi.setTime(String.valueOf(time));
      logApi.setMethodClass(methodClass);
      logApi.setMethodName(methodName);
      LogAbstractUtil.addRequestInfoToLog(request, logApi);
      Map<String, Object> event = new HashMap<>(16);
      event.put(EventConstant.EVENT_LOG, logApi);
      SpringUtil.publishEvent(new ApiLogEvent(event));
   }

}
  • 日志事件代码如下
/**
 * 系统日志事件
 *
 * @author Chill
 */
public class ApiLogEvent extends ApplicationEvent {

   public ApiLogEvent(Map<String, Object> source) {
      super(source);
   }

}
  • 日志监听器代码如下
/**
 * 异步监听日志事件
 *
 * @author Chill
 */
@Slf4j
@AllArgsConstructor
public class ApiLogListener {

   private final ILogClient logService;
   private final ServerInfo serverInfo;
   private final BladeProperties bladeProperties;


   @Async
   @Order
   @EventListener(ApiLogEvent.class)
   public void saveApiLog(ApiLogEvent event) {
      Map<String, Object> source = (Map<String, Object>) event.getSource();
      LogApi logApi = (LogApi) source.get(EventConstant.EVENT_LOG);
      LogAbstractUtil.addOtherInfoToLog(logApi, bladeProperties, serverInfo);
      logService.saveApiLog(logApi);
   }

}
  • 由此可见,思路其实同异常日志一致,都是获取请求相关信息后进行事件驱动解耦,异步将日志入库,若要增加其性能与并发能力,可以采用集成消息队列或者Redis,相信这些都难不倒大家